package cn.ges.aliasrandom.core;


import cn.ges.aliasrandom.aliasmethod.AliasMethod;

import java.io.Serializable;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.HashMap;

/**
 * 快速计算概率
 * <p>
 * 该类是线程安全的,可以多个线程共用一个对象,但是需要注意 -> 输入完毕概率后先调用一次getResult或者init方法
 * 如果这个类的概率会重复使用 请使用单例模式 同时和上一条一样 输入完毕概率后先调用一次getResult或者init方法
 *
 * @author gespent@163.com
 * @date 2019/2/20
 */
public class Probability<T> implements Serializable {

    private static final long serialVersionUID = 1L;

    private AliasMethod aliasMethod;
    private ArrayList<Double> odds;
    private HashMap<Integer, T> results;
    private Integer count = 0;
    private Double sum = 0D;
    private boolean alreadyGot = false;
    /**
     * 使用默认计算方式 -> 不需要概率总和=1 而是使用概率占比
     */
    private boolean useDefaultType = false;

    /**
     * 要求概率必须小于等于1 否则抛出异常
     */
    public Probability() {
        odds = new ArrayList<>(10);
        results = new HashMap<>(10);
    }

    /**
     * 自行控制概率 超过1 的概率和会导致 概率失真
     */
    public Probability(boolean useDefaultType) {
        odds = new ArrayList<>(10);
        results = new HashMap<>(10);
        this.useDefaultType = useDefaultType;
    }


    /**
     * 已知元素 数目的条件下 使用该方法来创建Probability
     *
     * @param elementQuantity 元素数目
     */
    public Probability(int elementQuantity, boolean useDefaultType) {
        odds = new ArrayList<>(elementQuantity);
        results = new HashMap<>(elementQuantity);
        this.useDefaultType = useDefaultType;
    }

    /**
     * 已知元素 数目的条件下 使用该方法来创建Probability
     *
     * @param elementQuantity 元素数目
     */
    public Probability(int elementQuantity) {
        odds = new ArrayList<>(elementQuantity);
        results = new HashMap<>(elementQuantity);
    }

    /**
     * 添加一种概率可能 当指向该概率时 会返回此时设置的result<br></>
     * 最后获取结果时，如果总概率不到1会自动补正到1,此时result为null<br></>
     * 当已经调用过getResult方法后 该方法将不会继续<br></>
     * useDefaultType时 忽略总概率必须为1条件
     *
     * @param probability 概率 0<probability<1 概率和应该小于1
     * @param result      指向该概率时 返回值
     * @return 操作的Probability对象
     */
    public Probability<T> addElement(Double probability, T result) {
        if (alreadyGot) {
            return this;
        }
        results.put(count++, result);
        odds.add(probability);
        if (!useDefaultType) {
            sum += probability;
        }
        return this;
    }

    /**
     * 重置当前已有的概率 重新构建 并删除某种结果(如果这种结果存在)
     * T需要自行实现eq方法
     * useDefaultType 时不做任何操作
     * 该方法线程不安全 不能共用一个对象进行操作
     */
    public void removeOne(T result) {
        if (useDefaultType) {
            return;
        }
        ArrayList<Double> newOdds = new ArrayList<>(10);
        HashMap<Integer, T> newResults = new HashMap<>(10);
        // 新的计数器
        int newCount = 0;
        Double newSum = 0D;

        for (Integer integer : results.keySet()) {
            T t = results.get(integer);
            if (t != null && !t.equals(result)) {
                // 塞入新的数据 跳过Null和result的数据
                newResults.put(newCount++, t);
                Double e = odds.get(integer);
                newOdds.add(e);
                newSum += e;
            }
        }
        odds = newOdds;
        results = newResults;
        sum = newSum;
        count = newCount;
        alreadyGot = false;
        init();
    }

    public void init() {
        if (useDefaultType) {
            this.alreadyGot = true;
            // 如果odd 为空 增加一个空项目
            if (odds.size() < 1) {
                odds.add(1D);
                results.put(1, null);
            }
        } else {
            checkOdds();
        }
    }

    /**
     * 获取结果前 检查概率是否有问题.并补正不到1的部分
     */
    private void checkOdds() {
        // 进行补正前 舍去sum的三位以后的小数
        this.sum = new BigDecimal(this.sum).setScale(3, RoundingMode.HALF_UP).doubleValue();
        if (this.sum == 1) {
            this.alreadyGot = true;
        } else if (this.sum < 1) {
            double probability = 1D - this.sum;
            this.addElement(probability, null);
            this.sum = 1D;
            this.alreadyGot = true;
        } else {
            throw new IllegalArgumentException("概率和不应该大于1!");
        }
    }

    /**
     * 获得结果<br></>
     * 如果概率和不为1 随机到不为1的部分将返回null
     */
    public T getResult() {
        if (!alreadyGot) {
            checkOdds();
        }
        if (this.aliasMethod == null) {
            this.aliasMethod = new AliasMethod(odds);
        }
        return results.get(this.aliasMethod.next());
    }

    /**
     * 获得结果且 不进行检查 适合缓存后的对象直接获得
     */
    public T getResultWithNoCheck() {
        if (this.aliasMethod == null) {
            this.aliasMethod = new AliasMethod(odds);
        }
        return results.get(this.aliasMethod.next());
    }



    public T getFirstResult() {
        return results.get(0);
    }


    public void removeAll(int elementQuantity) {
        reBuild();
        odds = new ArrayList<>(elementQuantity);
        results = new HashMap<>(elementQuantity);
    }

    public Double getSum() {
        return this.sum;
    }
    public void removeAll() {
        reBuild();
        odds = new ArrayList<>(10);
        results = new HashMap<>(10);
    }

    private void reBuild() {
        this.aliasMethod = null;
        this.alreadyGot = false;
        this.count = 0;
        this.sum = 0D;
    }

    public void setUseDefaultType(boolean useDefaultType) {
        this.useDefaultType = useDefaultType;
    }

    public ArrayList<Double> getOdds() {
        return odds;
    }

    public HashMap<Integer, T> getResults() {
        return results;
    }

    public boolean isAlreadyGot() {
        return alreadyGot;
    }

    public boolean isUseDefaultType() {
        return useDefaultType;
    }

    public void setAlreadyGot(boolean alreadyGot) {
        this.alreadyGot = alreadyGot;
    }
}
